package io.hilo.example.app.db;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.hilo.boot.core.UT;
import org.hilo.boot.core.db.DataSourceAopHelper;
import org.hilo.boot.core.db.DataSourceKeyHolder;
import org.hilo.boot.core.db.SelectDB;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.Order;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * 数据源AOP必须在事务注解之前，如果数据源AOP注解在Dao方法上，
 * 那么事务注解也必须在Dao方法上，不能在Service层，
 * 但是如果注解在Dao方法上，那么一个Dao的方法内部互相调用，AOP就不起作用，所以不要加在Dao上。
 * 建议都加在Service上，同时用AOP的切面表达式来控制Dao，
 * 这样Service的方法内部调用，最终调用Dao，仍然能够正常切换数据源，完美！
 * 
 * <p>
 * 设置事务执行顺序在切换数据源之后，否则只走默认DB，
 * 默认的事务优先级是最低的，所以随便设置一个Order值，都会优先于事务注解
 * @see EnableTransactionManagement#order
 * 
 * TODO 本配置为示例，请根据自身情况修改！！！！！！！！！！！
 * 
 * @author zollty
 * @since 2018年6月6日
 */
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=true)
@ConditionalOnProperty(name = "hilo.multi.datasource.names")
public class DynamicDataSourceAopConfig {
    private static final String DEFAULT_POINTCUTS_SELECTDB = 
            "io.hilo.example.demo.config.DefaultPointcuts.selectDB(anno)";
    private static final String DEFAULT_POINTCUTS_BASEMAPPER = 
            "io.hilo.example.demo.config.DefaultPointcuts.baseMapper()";
    private static final int ANNO_AOP_ORDER = -90;

    /** 用于Service层， 
     * <code>@SelectDB</code> 结合 
     * <code>@Transactional</code> 来进行 事务控制，
     * 如果不设置<code>@SelectDB</code>，将以默认数据源来开启事务 */
    @Aspect
    @Order(ANNO_AOP_ORDER)
    @Configuration
    class DbAopAnnotationConfig {
        @Before(DEFAULT_POINTCUTS_SELECTDB)
        public void switchDataSource(JoinPoint point, SelectDB anno) {
            DataSourceKeyHolder.setLookupKey(anno.value());
            debugPrint(point, "anno-switchDataSource");
        }

        @After(DEFAULT_POINTCUTS_SELECTDB)
        public void restoreDataSource(JoinPoint point, SelectDB anno) {
            DataSourceKeyHolder.removeCurrentKey();
            debugPrint(point, "anno-restoreDataSource");
        }
    }
    
    /** 读写分离，通过此AOP来控制数据源 */
    @Aspect
    // 先于 Annotation AOP 执行，后执行的 Annotation AOP 才能起作用。
    @Order(ANNO_AOP_ORDER - 10)
    //@Configuration
    class DbAopReadWriteSplittingConfig {
        @Before(DEFAULT_POINTCUTS_BASEMAPPER)
        public void switchDataSource(JoinPoint point) {
            DataSourceAopHelper.switchDataSourceRWSplit(point, null);
            debugPrint(point, "rwsplit-switchDataSource");
        }

        @After(DEFAULT_POINTCUTS_BASEMAPPER)
        public void restoreDataSource(JoinPoint point) {
            DataSourceKeyHolder.removeCurrentKey();
            debugPrint(point, "rwsplit-restoreDataSource");
        }
    }
    
    /** 
     * 根据package名称分离数据源，通过此AOP来控制数据源
     * <p>例如有两套Mapper代码：
     * <p>com.company.proj01.module1.dao.FooMapper，对应数据源名称：module1
     * <p>com.company.proj01.module2.dao.BarMapper，对应数据源名称：module2
     * 那么直接取出"module1"、"module2"作为数据源LookupKey，设置切换即可。
     */
    @Aspect
    // 先于 Annotation AOP 执行，后执行的 Annotation AOP 才能起作用。
    @Order(ANNO_AOP_ORDER - 10)
//    @Configuration //此为示例，需要时启用
    class DbAopPackageSplittingConfig {
        @Before(DEFAULT_POINTCUTS_BASEMAPPER)
        public void switchDataSource(JoinPoint point) {
            Class<?> targetClass = DataSourceKeyHolder.getMapperClassFromProxy(point.getTarget());
            String name;
            if (targetClass != null) {
                name = targetClass.getName();
            } else {
                name = point.getTarget().getClass().getName();
            }
            String[] arr = UT.Split.split(name, '.');
            // 倒数第三个'.'分割的路径名
            String ds = arr[arr.length - 3];
            DataSourceKeyHolder.setLookupKey(ds);
            debugPrint(name, point.getSignature().getName(), "packagesplit-switchDataSource");
        }

        @After(DEFAULT_POINTCUTS_BASEMAPPER)
        public void restoreDataSource(JoinPoint point) {
            DataSourceKeyHolder.removeCurrentKey();
            debugPrint(point, "packagesplit-restoreDataSource");
        }
    }
    
    static void debugPrint(JoinPoint point, String sign) {
        debugPrint(point.getTarget().getClass().getName(), 
                point.getSignature().getName(), sign);
    }
    
    static void debugPrint(String targetClass, String method, String sign) {
        System.out.println("----------------------------------------------");
        System.out.println("-------------------" + sign + " - " + targetClass + "." + method);
        System.out.println("----------------------------------------------");
    }

}
